using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Threading;
using System.Net;
using System.IO;
using SLMS;

namespace Sucrose.Base
{
    /// <summary>
    /// Struct to hold an imported grade for later integration to ImportableItem
    /// </summary>
    public struct ImportedGrade
    {
        public int taskId;
        public double grade;
    }

    /// <summary>
    /// Class used to operate with the LMS and its API
    /// </summary>
    public class LMSInterop
    {
        #region staticStuff

        private static LMSInterop instance = null;

        public static LMSInterop GetInstance()
        {
            if (instance == null)
                instance = new LMSInterop();
            return instance;
        }

        #endregion

        #region instanceVars

        private String email;
        private String firstName;
        private String lastName;
        private String userId;
        private bool authenticated = false;
        private DateTime lastAuthenticated = DateTime.MinValue;

        private SucroseLMS lmsObj = new SucroseLMS();

        private Course course;
        private List<CourseItem> courseItemList;

        private Dictionary<Course, Dictionary<int, double>> grades = new Dictionary<Course, Dictionary<int, double>>();
        private Dictionary<int, TaskCategory> categories = new Dictionary<int, TaskCategory>();
        private Dictionary<int, Term> terms = new Dictionary<int, Term>();

        #endregion

        #region basicProperties

        public String Email { get { return email; } }
        public String FirstName { get { return firstName; } }
        public String LastName { get { return lastName; } }
        public String UserId { get { return userId; } }
        public DateTime LastAuthenticated { get { return lastAuthenticated; } }

        #endregion

        #region events

        /// <summary>
        /// Updates to the progress of LMS Operations
        /// </summary>
        public event ProgressUpdateHandler ProgressUpdates;

        #endregion

        #region basicLMSStuff

        /// <summary>
        /// Authenticates the user with the LMS. This should pop up a dialog box from the RLMS.
        /// </summary>
        /// <param name="reAuthentication">Should we authenticate even if we already have? If yes, pass true.</param>
        /// <returns></returns>
        public bool Authenticate(bool reAuthentication)
        {
            if (reAuthentication)
            {
                authenticated = false;
            }

            do
            {
                try
                {
                    authenticated = lmsObj.Authenticate();
                }
                catch (WebException e)
                {
                    throw new SucroseException("LMS Error: " + e.Message, e);
                }
                catch (Exception e)
                {
                    if (e.Message == "User cancelled authentication")
                        throw new ActionCancelledException("User cancelled LMS Authentication", e);
                    else
                        throw new SucroseException(e.Message, e);
                }
                if (!authenticated )
                    Logger.Log("Invalid logon", MessageType.Warning);

            } while (!authenticated);

            lastAuthenticated = DateTime.Now;
            email = lmsObj.Email;
            firstName = lmsObj.FirstName;
            lastName = lmsObj.LastName;
            userId = lmsObj.UserId;

            return authenticated;
        }

        /// <summary>
        /// Gets an XML document from the LMS API
        /// </summary>
        /// <param name="documentType">The type of XML to download</param>
        /// <param name="key">The neame of the GET variable to pass</param>
        /// <param name="value">The value for the key</param>
        /// <returns>The XML Document requested</returns>
        public XmlDocument DownloadXML(API_URL documentType, String key, String value)
        {
            XmlDocument retDoc = null;

            if (!authenticated)
                Authenticate(false);

            try
            {
                if (String.IsNullOrEmpty(key))
                    retDoc = lmsObj.GetXmlDocument(documentType, null);
                else
                    retDoc = lmsObj.GetXmlDocument(documentType, key + "=" + value);
            }
            catch (WebException e)
            {
                throw new SucroseException("LMS has timed out", e);
            }

            if (retDoc.DocumentElement.Name == "errors")
                throw new SucroseException(retDoc.DocumentElement.InnerText);

            if (retDoc == null)
                throw new SucroseException("LMS Did not respond with requested document");

            return retDoc;
        }

        /// <summary>
        /// Gets an XML document from the LMS API without a parameter
        /// </summary>
        /// <param name="documentType">The type of XML to download</param>
        /// <returns>The XML Document requested</returns>
        public XmlDocument DownloadXML(API_URL documentType)
        {
            return DownloadXML(documentType, null, null);
        }

        /// <summary>
        /// Downloads a general document from the LMS and writes it to disk
        /// </summary>
        /// <param name="remote">URL of remote resource</param>
        /// <param name="local">Local filename to save document to</param>
        /// <param name="overwrite">If true, this method will overwrite any file of the same name at the local filename</param>
        public void DownloadDocument(String remote, String local, bool overwrite)
        {
            if (!authenticated)
                Authenticate(false);

            HttpWebRequest request = null;
            HttpWebResponse response = null;

            try
            {
                request = (HttpWebRequest)WebRequest.Create(remote);

                request.UserAgent = "Sucrose / 4.1";
                request.Referer = "http://sucrose.googlecode.com/";

                response = (HttpWebResponse)lmsObj.GetResponse(request);

                if (response.StatusCode == HttpStatusCode.OK && (overwrite || !File.Exists(local)))
                {
                    FileStream fStream = null;
                    Stream wStream = null;
                    BinaryReader wReader = null;
                    BinaryWriter fWriter = null;

                    try
                    {
                        String path = local.Replace(Util.GetFilenameFromPath(local), "");
                        if (!Directory.Exists(path))
                            Directory.CreateDirectory(path);

                        fStream = new FileStream(local, FileMode.Create);
                        wStream = response.GetResponseStream();

                        wReader = new BinaryReader(wStream);
                        fWriter = new BinaryWriter(fStream);

                        const int bufferSize = 1024;
                        byte[] buffer = new byte[bufferSize];
                        int bytesRead = 0;
                        while ((bytesRead = wReader.Read(buffer, 0, bufferSize)) > 0)
                        {
                            fWriter.Write(buffer, 0, bytesRead);
                            bytesRead--;
                        }
                        fWriter.Flush();

                    }
                    catch (IOException ex)
                    {
                        try
                        {
                            if (File.Exists(local))
                                File.Delete(local);
                        }
                        catch (IOException) { } //Silence this exception. We shouldnt delete it if we can't access it anyway.

                        Logger.Log(ex.Message, MessageType.Warning);
                    }
                    finally
                    {
                        if (fWriter != null)
                            fWriter.Close();
                        if (wReader != null)
                            wReader.Close();
                        if (fStream != null && fWriter == null)
                            fStream.Close();
                        if (wStream != null && wReader == null)
                            wStream.Close();
                    }
                }
            }
            catch (WebException ex)
            {
                Logger.Log(ex.Message, MessageType.Warning);
            }
            finally
            {
                if (response != null)
                    response.Close();
            }
        }

        /// <summary>
        /// Downloads a general document from the LMS and writes it to disk
        /// </summary>
        /// <param name="remote">URL of remote resource</param>
        /// <param name="local">Local filename to save document to</param>
        public void DownloadDocument(String remote, String local)
        {
            DownloadDocument(remote, local, true);
        }

        #endregion

        #region courseAssembly

        /// <summary>
        /// Downloads and parses a list of current Courses from the LMS
        /// </summary>
        /// <returns>A list of current courses</returns>
        public List<Course> GetCourses()
        {
            List<Course> retList = new List<Course>();

            XmlDocument coursesDocument = DownloadXML(API_URL.GetCurrentClasses);

            if (coursesDocument == null)
            {
                throw new SucroseException("Error getting current classes");
            }
            else
                Logger.Log("Class information successfully downloaded", MessageType.Debug);

            IEnumerator courseEnumerator = coursesDocument.GetElementsByTagName("class").GetEnumerator();

            int totalCourseCount = Util.getEnumeratorCount(courseEnumerator);
            int currentCourse = 0;

            while (courseEnumerator.MoveNext())
            {
                try
                {
                    Course current = ParseCourseFromXml((XmlNode)courseEnumerator.Current);

                    // Get, sort, and link all the tasks for this course
                    current.AllItems = this.GetCourseItems(current);
                    this.SortItems(current);
                    this.LinkTasks(current);
                    this.CalculateGrades(current);

                    retList.Add(current);

                    Logger.Log("Created class: " + current.CourseNumber, MessageType.Info);
                }
                catch (SucroseException ex)
                {
                    Logger.Log("Error importing Course! - " + ex.Message, MessageType.Error);
                }
                
                if (ProgressUpdates != null)
                    ProgressUpdates.Invoke(++currentCourse, totalCourseCount);
            }

            this.grades = new Dictionary<Course, Dictionary<int, double>>();
            this.categories = new Dictionary<int, TaskCategory>();
            this.terms = new Dictionary<int, Term>();

            return retList;
        }

        /// <summary>
        /// Sorts CourseItems into DownloadableItems and TaskItems
        /// </summary>
        private void SortItems(Course course)
        {
            foreach (CourseItem item in course.AllItems)
            {
                item.Course = course;

                DownloadableItem downloadableItem = item as DownloadableItem;
                TaskItem taskItem = item as TaskItem;

                if (downloadableItem != null)
                    course.DownloadableItems.Add(downloadableItem);
                if (taskItem != null)
                    course.TaskItems.Add(taskItem);
            }
        }

        /// <summary>
        /// Calculates and stores grades for the course
        /// </summary>
        private void CalculateGrades(Course course)
        {
            double estimatedTotalPoints = 0;
            double estimatedEarnedPoints = 0;

            foreach (TaskItem task in course.TaskItems)
            {
                if (task.Grade != -1 && !task.IsChild && !task.IsParent)
                {
                    double points = (task.GradeWeight / 100.0) * task.Category.GradeWeight;
                    estimatedTotalPoints += points;
                    estimatedEarnedPoints += (task.Grade / 100) * points;
                }
                else if (task.Grade != -1 && task.IsChild)
                {
                    double points = (task.ParentTask.GradeWeight / 100.0) * (task.GradeWeight / 100.0) * task.Category.GradeWeight;
                    estimatedTotalPoints += points;
                    estimatedEarnedPoints += (task.Grade / 100) * points;
                }
            }

            if (estimatedTotalPoints > 0)
            {
                double percentage = estimatedEarnedPoints / estimatedTotalPoints;

                course.EstimatedGradePercent = percentage;
                course.EstimatedGradeLetter = course.PercentageToGrade(percentage);
            }
            else
                course.EstimatedGradeLetter = "No grades available";

            double totalPoints = 0;
            double earnedPoints = 0;

            foreach (TaskItem task in course.TaskItems)
            {
                if (task.Grade != -1 && !task.IsChild)
                {
                    double points = (task.GradeWeight / 100.0) * task.Category.GradeWeight;
                    totalPoints += points;
                    earnedPoints += (task.Grade / 100) * points;
                }
            }

            if (totalPoints > 0)
            {
                double percentage = earnedPoints / totalPoints;

                course.GradePercent = percentage;
                course.GradeLetter = course.PercentageToGrade(percentage);
            }
            else
                course.GradeLetter = "No grades available";
        }

        /// <summary>
        /// Links a Course's tasks with parents, children, Categories and Grades
        /// </summary>
        private void LinkTasks(Course course)
        {
            foreach (TaskItem task in course.TaskItems)
            {
                if (task.TempParentTask != 0)
                {
                    TaskItem parent = FindTask(course, task.TempParentTask);
                    if (parent != null)
                        task.ParentTask = parent;
                    parent.IsParent = true;
                    task.IsChild = true;
                }
                if (task.TempCategory != 0)
                {
                    try
                    {
                        task.Category = this.categories[task.TempCategory];
                    }
                    catch (KeyNotFoundException) { }
                }
            }
            foreach (int taskId in this.grades[course].Keys)
            {
                try
                {
                    TaskItem task = this.FindTask(course, taskId);

                    if (task != null)
                        task.Grade = grades[course][taskId];
                }
                catch (KeyNotFoundException) { }
            }
        }

        /// <summary>
        /// Gets a Task for a given Id.
        /// </summary>
        /// <param name="id">The Id of the desired Task</param>
        /// <returns>Task with given id. Null if no task found</returns>
        private TaskItem FindTask(Course course, int id)
        {
            foreach (TaskItem task in course.TaskItems)
            {
                if (task.Id == id)
                    return task;
            }
            return null;
        }

        /// <summary>
        /// Downloads and parses items related to a Course
        /// </summary>
        /// <param name="course">The Course to get items for</param>
        /// <returns>A list of CourseItems</returns>
        private List<CourseItem> GetCourseItems(Course course)
        {
            courseItemList = new List<CourseItem>();

            try
            {
                this.course = course;

                //Threading removed as many problems were being caused, and performance is not largely affected.

                DownloadCategories();
                DownloadTasks();
                DownloadSupport();
                DownloadGrades();
            }
            catch (SucroseException ex)
            {
                throw ex;
            }

            return courseItemList;
        }

        #endregion

        #region downloaders

        /// <summary>
        /// Downloads all tasks for the current working Course
        /// </summary>
        private void DownloadTasks()
        {
            XmlDocument taskXML = DownloadXML(API_URL.GetTasks, "class_id", course.Id.ToString());

            IEnumerator taskEnumerator = taskXML.GetElementsByTagName("task").GetEnumerator();

            while (taskEnumerator.MoveNext())
            {
                TaskItem item = ParseTaskItemFromXML((XmlNode)taskEnumerator.Current);
                if (item != null)
                    courseItemList.Add(item);
            }
        }

        /// <summary>
        /// Downloads all TaskCategories for the current working Course
        /// </summary>
        private void DownloadCategories()
        {
            XmlDocument categoriesXML = DownloadXML(API_URL.GetTaskCategories, "class_id", course.Id.ToString());

            IEnumerator categoryEnumerator = categoriesXML.GetElementsByTagName("category").GetEnumerator();

            while (categoryEnumerator.MoveNext())
            {
                TaskCategory category = ParseTaskCategoryFromXML((XmlNode)categoryEnumerator.Current);
                if (category != null)
                    this.categories.Add(category.Id, category);
            }
        }

        /// <summary>
        /// Downloads all SupportDocument specifications for the current working Course
        /// </summary>
        private void DownloadSupport()
        {
            XmlDocument supportXML = DownloadXML(API_URL.GetSupportMaterials, "class_id", course.Id.ToString());

            IEnumerator supportEnumerator = supportXML.GetElementsByTagName("material").GetEnumerator();

            while (supportEnumerator.MoveNext())
            {
                SupportItem item = ParseSupportFromXML((XmlNode)supportEnumerator.Current);
                if (item != null)
                    courseItemList.Add(item);
            }
        }

        /// <summary>
        /// Downloads all Grades for the current working Course
        /// </summary>
        private void DownloadGrades()
        {
            XmlDocument gradesXML = DownloadXML(API_URL.GetGrades, "class_id", course.Id.ToString());

            IEnumerator gradesEnumerator = gradesXML.GetElementsByTagName("grade").GetEnumerator();

            bool odd = true;

            Dictionary<int, double> gradesForClass = new Dictionary<int, double>();

            while (gradesEnumerator.MoveNext())
            {
                // We do this because the root grade node is the same as the child grade node name.
                if (odd)
                {
                    ImportedGrade grade = ParseGradeFromXML((XmlNode)gradesEnumerator.Current);
                    try
                    {
                        if (grade.taskId != -1)
                            gradesForClass.Add(grade.taskId, grade.grade);
                    }
                    catch (ArgumentException) { }
                }
                odd = !odd;
            }

            grades.Add(course, gradesForClass);
        }

        #endregion

        #region basicParsers

        /// <summary>
        /// Parses a Course object from an XML Node
        /// </summary>
        /// <param name="courseNode">A node containing a single Course to parse</param>
        /// <returns>A Course object</returns>
        private Course ParseCourseFromXml(XmlNode courseNode)
        {
            Course course = new Course();
            double[] letterGrades = new double[11];
            bool validCourse = false;

            IEnumerator enumerator = courseNode.GetEnumerator();

            while (enumerator.MoveNext())
            {
                XmlNode current = (XmlNode)enumerator.Current;
                String value = current.InnerText;

                switch (current.Name)
                {
                    case "class_id":
                        try
                        {
                            course.Id = Int32.Parse(value);
                            validCourse = true;
                        }
                        catch (FormatException)
                        {
                            validCourse = false;
                        }
                        break;
                    case "term_id":
                        try
                        {
                            int termId = Int32.Parse(value);
                            course.Term = this.terms[termId];
                        }
                        catch (FormatException) { }
                        catch (KeyNotFoundException) { }
                        break;
                    case "course_numbers":
                        course.CourseNumber = value;
                        break;
                    case "class_name":
                        course.CourseName = value;
                        break;
                    case "url":
                        course.Url = value;
                        break;
                    case "description":
                        course.Description = value;
                        break;
                    case "user_role":
                        course.UserRole = Course.StringRoleToUserRole(value);
                        break;
                    case "grade_conversion":
                        course.ConversionMethod = Course.StringConversionToGradeConversion(value);
                        break;
                    case "pass_range_start":
                        try
                        {
                            course.PassRangeStart = Double.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                    case "grade_a_start":
                        Double.TryParse(value, out letterGrades[0]);
                        break;
                    case "grade_a_minus_start":
                        Double.TryParse(value, out letterGrades[1]);
                        break;
                    case "grade_b_plus_start":
                        Double.TryParse(value, out letterGrades[2]);
                        break;
                    case "grade_b_start":
                        Double.TryParse(value, out letterGrades[3]);
                        break;
                    case "grade_b_minus_start":
                        Double.TryParse(value, out letterGrades[4]);
                        break;
                    case "grade_c_plus_start":
                        Double.TryParse(value, out letterGrades[5]);
                        break;
                    case "grade_c_start":
                        Double.TryParse(value, out letterGrades[6]);
                        break;
                    case "grade_c_minus_start":
                        Double.TryParse(value, out letterGrades[7]);
                        break;
                    case "grade_d_plus_start":
                        Double.TryParse(value, out letterGrades[8]);
                        break;
                    case "grade_d_start":
                        Double.TryParse(value, out letterGrades[9]);
                        break;
                    case "grade_d_minus_start":
                        Double.TryParse(value, out letterGrades[10]);
                        break;
                }
            }

            if (!validCourse)
                return null;
            else
            {
                course.LetterGrades = letterGrades;
                return course;
            }
        }

        /// <summary>
        /// Parses a TaskItem object from an XML Node
        /// </summary>
        /// <param name="taskNode">An node containting a single TaskItem to parse</param>
        /// <returns>A TaskItem object</returns>
        private TaskItem ParseTaskItemFromXML(XmlNode taskNode)
        {
            TaskItem task = new TaskItem();

            bool validTask = false;

            IEnumerator enumerator = taskNode.GetEnumerator();

            while (enumerator.MoveNext())
            {
                XmlNode current = (XmlNode)enumerator.Current;
                String value = current.InnerText;

                switch (current.Name)
                {
                    case "task_id":
                        try
                        {
                            task.Id = Int32.Parse(value);
                            validTask = true;
                        }
                        catch (FormatException)
                        {
                            validTask = false;
                        }
                        break;
                    case "parent_task_id":
                        try
                        {
                            task.TempParentTask = Int32.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                    case "category_id":
                        try
                        {
                            task.TempCategory = Int32.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                    case "task_name":
                        task.Name = value;
                        break;
                    case "url":
                        task.Url = value;
                        break;
                    case "description":
                        task.Description = value;
                        break;
                    case "file":
                        task.RemoteLocation = value;
                        break;
                    case "due_date":
                        try
                        {
                            task.DueDate = DateTime.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                    case "grade_weight":
                        try
                        {
                            task.GradeWeight = Double.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                    case "last_modified":
                        try
                        {
                            task.LastModified = DateTime.Parse(value);
                        }
                        catch (FormatException)
                        {
                            task.LastModified = DateTime.MaxValue;
                        }
                        break;
                }
            }

            if (!validTask)
                return null;
            return task;
        }

        /// <summary>
        /// Parses a TaskCategory from a node
        /// </summary>
        /// <param name="categoryNode">Node containting a TaskCategory</param>
        private TaskCategory ParseTaskCategoryFromXML(XmlNode categoryNode)
        {
            TaskCategory category = new TaskCategory();

            bool validCategory = false;

            IEnumerator enumerator = categoryNode.GetEnumerator();

            while (enumerator.MoveNext())
            {
                XmlNode current = (XmlNode)enumerator.Current;
                String value = current.InnerText;

                switch (current.Name)
                {
                    case "category_id":
                        try
                        {
                            category.Id = Int32.Parse(value);
                            validCategory = true;
                        }
                        catch(FormatException)
                        {
                            validCategory = false;
                        }
                        break;
                    case "category_name":
                        category.Name = value;
                        break;
                    case "category_name_plural":
                        category.NamePlural = value;
                        break;
                    case "grade_weight":
                        try
                        {
                            category.GradeWeight = Double.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                }
            }

            if (!validCategory)
                return null;
            else
                return category;
        }

        /// <summary>
        /// Parses a SupportDocument from a node
        /// </summary>
        /// <param name="supportNode">Node containing a single SupportDocument</param>
        /// <returns>A SupportDocument</returns>
        private SupportItem ParseSupportFromXML(XmlNode supportNode)
        {
            SupportItem item = new SupportItem();

            bool validSupport = false;

            IEnumerator enumerator = supportNode.GetEnumerator();

            while (enumerator.MoveNext())
            {
                XmlNode current = (XmlNode)enumerator.Current;
                String value = current.InnerText;

                switch (current.Name)
                {
                    case "file_id":
                        try
                        {
                            item.Id = Int32.Parse(value);
                                validSupport = true;
                        }
                        catch (FormatException)
                        {
                            validSupport = false;
                        }
                        break;
                    case "title":
                        item.Name = value;
                        break;
                    case "url":
                        item.Url = value;
                        break;
                    case "description":
                        item.Description = value;
                        break;
                    case "file":
                        item.RemoteLocation = value;
                        break;
                    case "type":
                        item.Type = value;
                        break;
                    case "last_modified":
                        try
                        {
                            item.LastModified = DateTime.Parse(value);
                        }
                        catch (FormatException)
                        {
                            item.LastModified = DateTime.MaxValue;
                        }
                        break;
                }
            }

            if (!validSupport)
                return null;
            else
                return item;
        }

        /// <summary>
        /// Parses a grade from a node
        /// </summary>
        /// <param name="gradeNode">Node containing a single grade</param>
        /// <returns>An ImportedGrade</returns>
        private ImportedGrade ParseGradeFromXML(XmlNode gradeNode)
        {
            ImportedGrade importedGrade = new ImportedGrade();
            importedGrade.taskId = -1;
            importedGrade.grade = 0;

            IEnumerator enumerator = gradeNode.GetEnumerator();

            while (enumerator.MoveNext())
            {
                XmlNode current = (XmlNode)enumerator.Current;
                String value = current.InnerText;

                switch (current.Name)
                {
                    case "task_id":
                        Int32.TryParse(value, out importedGrade.taskId);
                        break;
                    case "grade":
                        Double.TryParse(value, out importedGrade.grade);
                        break;
                }
            }

            return importedGrade;
        }

        /// <summary>
        /// Parses a term from a node
        /// </summary>
        /// <param name="gradeNode">Node containing a single term</param>
        /// <returns>A new Term</returns>
        private Term ParseTermFromXML(XmlNode gradeNode, int id)
        {
            Term term = new Term();
            term.Id = id;

            IEnumerator enumerator = gradeNode.GetEnumerator();

            while (enumerator.MoveNext())
            {
                XmlNode current = (XmlNode)enumerator.Current;
                String value = current.InnerText;

                switch (current.Name)
                {
                    case "term_name":
                        term.Name = value;
                        break;
                    case "start_date":
                        try
                        {
                            term.Start = DateTime.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                    case "end_date":
                        try
                        {
                            term.End = DateTime.Parse(value);
                        }
                        catch (FormatException) { }
                        break;
                }
            }

            return term;
        }

        #endregion

    }
}
